Authentication & Error Handling Standard
Overview
This document defines the standard patterns for authentication and error handling in API routes. Consistent patterns improve security, user experience, and maintainability.
Authentication Standard
Helper Location
**File**: src/lib/auth/get-authenticated-user.ts
Standard Pattern
import { getAuthenticatedUser, AuthError } from '@/lib/auth/get-authenticated-user'
import { sendApiError, sendApiSuccess, withApiHandler } from '@/lib/api/api-response'
// ✅ GOOD: Using helper
export async function GET(request: Request) {
return withApiHandler(async () => {
const user = await getAuthenticatedUser(request as NextRequest)
if (!user) {
throw new AuthError('Unauthorized', 'UNAUTHORIZED', 401)
}
// User is authenticated, proceed with logic
return sendApiSuccess({ userId: user.id })
})
}Authentication Methods
1. Get Authenticated User
import { getAuthenticatedUser } from '@/lib/auth/get-authenticated-user'
const user = await getAuthenticatedUser(request)
if (!user) {
return sendApiError(401, 'Unauthorized', 'UNAUTHORIZED')
}
// User is authenticated
console.log(user.id, user.email)2. Get User or Throw
import { getAuthenticatedUserOrThrow } from '@/lib/auth/get-authenticated-user'
try {
const user = await getAuthenticatedUserOrThrow(request)
// User is guaranteed to be authenticated
console.log(user.id)
} catch (error) {
if (error instanceof AuthError) {
return sendApiError(error.statusCode, error.message, error.code)
}
}3. Require Auth
import { requireAuth } from '@/lib/auth/get-authenticated-user'
export async function GET(request: Request) {
return withApiHandler(async () => {
const user = await requireAuth(request as NextRequest)
// User is guaranteed to be authenticated
return sendApiSuccess({ userId: user.id })
})
}4. Require Role
import { requireRole } from '@/lib/auth/get-authenticated-user'
export async function GET(request: Request) {
return withApiHandler(async () => {
const user = await requireRole(request as NextRequest, 'admin')
// User is guaranteed to be admin
return sendApiSuccess({ userId: user.id })
})
}5. Require Any Role
import { requireAnyRole } from '@/lib/auth/get-authenticated-user'
export async function GET(request: Request) {
return withApiHandler(async () => {
const user = await requireAnyRole(request as NextRequest, ['admin', 'owner'])
// User is guaranteed to be admin or owner
return sendApiSuccess({ userId: user.id })
})
}Before vs After
Before (Inconsistent)
// ❌ BAD: Manual session check
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions)
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
// ... rest of logic
}
// ❌ BAD: Different error format
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions)
if (!session || !session.user) {
return NextResponse.json(
{ error: 'UNAUTHORIZED', code: 'AUTH_FAILED' },
{ status: 401 }
)
}
// ... rest of logic
}After (Standard)
// ✅ GOOD: Using helper with standard error
import { requireAuth } from '@/lib/auth/get-authenticated-user'
import { withApiHandler, sendApiSuccess } from '@/lib/api/api-response'
export async function GET(request: Request) {
return withApiHandler(async () => {
const user = await requireAuth(request as NextRequest)
return sendApiSuccess({ userId: user.id })
})
}
export async function POST(request: Request) {
return withApiHandler(async () => {
const user = await requireAuth(request as NextRequest)
return sendApiSuccess({ userId: user.id })
})
}Error Handling Standard
Helper Location
**File**: src/lib/api/api-response.ts
Standard Pattern
import { withApiHandler, sendApiError, sendApiSuccess, Errors } from '@/lib/api/api-response'
// ✅ GOOD: Using withApiHandler wrapper
export async function GET(request: Request) {
return withApiHandler(async () => {
// All errors automatically caught and formatted
const data = await someOperation()
return sendApiSuccess(data)
})
}Error Handling Methods
1. Using withApiHandler
import { withApiHandler, sendApiSuccess } from '@/lib/api/api-response'
export async function GET(request: Request) {
return withApiHandler(async () => {
// Errors automatically caught and formatted
const result = await getData()
return sendApiSuccess(result)
})
}2. Throwing ApiError
import { withApiHandler, ApiError, sendApiSuccess } from '@/lib/api/api-response'
export async function GET(request: Request) {
return withApiHandler(async () => {
const data = await getData()
if (!data) {
throw new ApiError(404, 'NOT_FOUND', 'Data not found')
}
return sendApiSuccess(data)
})
}3. Using Errors Object
import { withApiHandler, Errors, sendApiSuccess } from '@/lib/api/api-response'
export async function POST(request: Request) {
return withApiHandler(async () => {
const body = await request.json()
if (!body.name) {
throw Errors.badRequest('Name is required')
}
const result = await createData(body)
return sendApiSuccess(result, 201)
})
}4. Manual Error Handling
import { handleApiError, sendApiSuccess } from '@/lib/api/api-response'
export async function GET(request: Request) {
try {
const result = await getData()
return sendApiSuccess(result)
} catch (error) {
return handleApiError(error, 'GET /api/data')
}
}Common Error Types
import { Errors } from '@/lib/api/api-response'
// Authentication errors
throw Errors.unauthorized('You must log in')
throw Errors.forbidden('Access denied')
// Validation errors
throw Errors.badRequest('Invalid input')
throw Errors.validation({ field: 'error' })
// Resource errors
throw Errors.notFound('User')
throw Errors.conflict('Resource already exists')
// Rate limiting
throw Errors.rateLimited()
// Payment errors
throw Errors.paymentRequired('Upgrade required')
// Server errors
throw Errors.internal('Something went wrong')Complete Example
import {
withApiHandler,
withTenantContext,
sendApiSuccess,
Errors
} from '@/lib/api/api-response'
import { requireAuth } from '@/lib/auth/get-authenticated-user'
export async function GET(request: Request) {
return withApiHandler(async () => {
// Authentication
const user = await requireAuth(request as NextRequest)
// Tenant context
return withTenantContext(async ({ id: tenantId }) => {
// Business logic
const agents = await getAgents(tenantId)
// Success response
return sendApiSuccess(agents)
}, request)
})
}
export async function POST(request: Request) {
return withApiHandler(async () => {
// Authentication
const user = await requireAuth(request as NextRequest)
// Validation
const body = await request.json()
if (!body.name) {
throw Errors.badRequest('Name is required')
}
// Tenant context
return withTenantContext(async ({ id: tenantId }) => {
// Business logic
const result = await createAgent(tenantId, body)
// Success response
return sendApiSuccess(result, 201)
}, request)
})
}Best Practices
1. Always Use withApiHandler
// ✅ GOOD: Errors automatically handled
export async function GET(request: Request) {
return withApiHandler(async () => {
const data = await getData()
return sendApiSuccess(data)
})
}
// ❌ BAD: Manual error handling
export async function GET(request: Request) {
try {
const data = await getData()
return sendApiSuccess(data)
} catch (error) {
return handleApiError(error, 'GET')
}
}2. Use Auth Helpers
// ✅ GOOD: Using helper
const user = await requireAuth(request)
// ❌ BAD: Manual session check
const session = await getServerSession(authOptions)
if (!session) {
return sendApiError(401, 'Unauthorized')
}3. Include Error Codes
// ✅ GOOD: Descriptive error code
throw Errors.notFound('Agent', { agentId: '123' })
// Returns: { error: 'Agent not found', code: 'NOT_FOUND', details: {...} }
// ❌ BAD: Generic error
throw new Error('Not found')
// Returns: { error: 'Not found', code: 'INTERNAL_ERROR' }4. Use Tenant Context Helper
// ✅ GOOD: Tenant context guaranteed
return withTenantContext(async ({ id: tenantId }) => {
// Guaranteed to have tenant
const data = await getData(tenantId)
return sendApiSuccess(data)
}, request)
// ❌ BAD: Manual tenant extraction
const tenant = await getTenantFromRequest(request)
if (!tenant) {
return sendApiError(404, 'Tenant not found')
}
const data = await getData(tenant.id)5. Chain Wrappers
// ✅ GOOD: Chained wrappers
export async function POST(request: Request) {
return withApiHandler(async () => {
return withTenantContext(async ({ id: tenantId }) => {
return withRateLimit(async () => {
const result = await doSomething(tenantId)
return sendApiSuccess(result)
}, tenantId, redis)
}, request)
})
}Migration Guide
Before
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
export async function GET(req: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const tenant = await getTenantFromRequest(req)
if (!tenant) {
return NextResponse.json(
{ error: 'Tenant not found' },
{ status: 404 }
)
}
const data = await getData(tenant.id)
return NextResponse.json(data)
} catch (error) {
console.error('Error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}After
import {
withApiHandler,
withTenantContext,
sendApiSuccess,
Errors
} from '@/lib/api/api-response'
import { requireAuth } from '@/lib/auth/get-authenticated-user'
export async function GET(request: Request) {
return withApiHandler(async () => {
return withTenantContext(async ({ id: tenantId }) => {
const data = await getData(tenantId)
return sendApiSuccess(data)
}, request)
})
}Testing
Test Authentication
import { requireAuth } from '@/lib/auth/get-authenticated-user'
test('requires authentication', async () => {
const request = new Request('http://localhost')
await expect(requireAuth(request)).rejects.toThrow('Unauthorized')
})Test Error Handling
import { withApiHandler, Errors } from '@/lib/api/api-response'
test('handles errors', async () => {
const handler = withApiHandler(async () => {
throw Errors.notFound('Resource')
})
const response = await handler(new Request('http://localhost'))
const data = await response.json()
expect(response.status).toBe(404)
expect(data.code).toBe('NOT_FOUND')
})Security
Always Check Authentication
// ✅ GOOD: Auth check required
export async function DELETE(request: Request) {
return withApiHandler(async () => {
const user = await requireAuth(request)
// Safe to proceed
await deleteSomething(user.id)
return sendApiSuccess({ success: true })
})
}
// ❌ BAD: No auth check
export async function DELETE(request: Request) {
return withApiHandler(async () => {
// Anyone can delete!
await deleteSomething(anyoneId)
return sendApiSuccess({ success: true })
})
}Use Tenant Isolation
// ✅ GOOD: Tenant isolation guaranteed
return withTenantContext(async ({ id: tenantId }) => {
const data = await db.query(
'SELECT * FROM agents WHERE tenant_id = $1',
[tenantId]
)
return sendApiSuccess(data)
}, request)
// ❌ BAD: No tenant isolation
const data = await db.query('SELECT * FROM agents')
// Returns all tenants' data!References
- Auth Helper:
src/lib/auth/get-authenticated-user.ts - API Response:
src/lib/api/api-response.ts - NextAuth: https://next-auth.js.org/
- API Standard:
docs/API_RESPONSE_STANDARD.md
Changelog
- 2026-02-08: Initial standard created
- 2026-02-08: Auth helper implemented
- 2026-02-08: Error handling patterns documented